Skip to content

Reopen profile picker on reselect + WebDAV icons#42

Merged
hawkff merged 2 commits into
mainfrom
feature/picker-reselect-webdav-icons
Jun 20, 2026
Merged

Reopen profile picker on reselect + WebDAV icons#42
hawkff merged 2 commits into
mainfrom
feature/picker-reselect-webdav-icons

Conversation

@hawkff

@hawkff hawkff commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Adopts two upstream (starifly) commits, cherry-picked cleanly onto our fork:

  1. Reopen profile picker on reselect (196257c) — fixes a real UX bug in the outbound / front-proxy / landing-proxy / route-rule dropdowns. When the current value is already "Select profile", Android's Spinner does not fire onItemSelected on re-tap, so the profile chooser couldn't be reopened to pick a different profile without first switching to another option and back. Adds a ReselectableSpinner (fires the selection callback on re-select), pre-selects the current profile in ProfileSelectActivity via EXTRA_SELECTED, and replaces the magic string "3" with a named OutboundPreference.VALUE_SELECT_PROFILE constant.
  2. Add WebDAV icons (18b4bc5) — cosmetic: adds row icons to the WebDAV settings screen (one new drawable ic_baseline_folder_open_24; the other four icons already existed).

Notes

  • Both apply cleanly to main (no conflicts).
  • Verified ProfileSelectActivity.EXTRA_SELECTED already exists and is handled; the new reselectable dropdown layout mirrors androidx's preference_dropdown_material.xml.
  • Added a KDoc to ReselectableSpinner.onWindowFocusChanged documenting that window-focus is a best-effort proxy for popup dismissal (may fire on app foregrounding, IME, system dialogs, rotation); the reselect logic stays correct if it fires spuriously.

Testing

  • assembleOssDebug builds clean; CodeRabbit local review: 0 findings.
  • Installed on device (arm64-v8a, API 33).

Not for merge yet — pending on-device verification of the reselect behavior.

Greptile Summary

This PR cherry-picks two upstream commits: a UX fix that allows the "Select Profile" spinner to be reopened when the current value is already "Select Profile" (by introducing ReselectableSpinner and wiring it into OutboundPreference), and a cosmetic change that adds row icons to the WebDAV settings screen.

  • Reselectable spinner: A new ReselectableSpinner overrides both setSelection overloads to re-fire onItemSelected when the same position is selected. OutboundPreference gains a dropdownOpened flag (set in onClick, cleared after each selection or popup close) and a selectionReady post-guard to distinguish initial binding from live interaction. GroupSettingsActivity and RouteSettingsActivity are updated to pass the currently-selected profile as EXTRA_SELECTED so the picker pre-selects the right row on reopen.
  • WebDAV icons: Five app:icon attributes added to webdav_preferences.xml; only ic_baseline_folder_open_24 is new.
  • Magic constant removed: The hard-coded "3" for the "select profile" spinner index is replaced by OutboundPreference.VALUE_SELECT_PROFILE throughout.

Confidence Score: 4/5

Safe to merge after on-device verification of the reselect path; the WebDAV icon change is entirely cosmetic and risk-free.

The core logic is sound and well-guarded. The two things worth watching are: (1) ReselectableSpinner.setSelection(Int) delegates to super which bounces back through setSelection(Int, Boolean) via virtual dispatch, so notifyReselected fires twice per same-position selection — currently harmless because the first fire clears dropdownOpened, but it means callers receive onItemSelected twice for one gesture; and (2) a narrow race where a notification banner or IME event lands between the user opening the dropdown and re-tapping 'Select Profile' will silently swallow that tap. Both are documented limitations, not unknown gaps. No data-loss or crash risk is introduced.

ReselectableSpinner.kt warrants a second look on the double-notification path and the window-focus proxy behaviour before this is promoted to a release build.

Important Files Changed

Filename Overview
app/src/main/java/io/nekohasekai/sagernet/widget/ReselectableSpinner.kt New custom Spinner that fires onItemSelected on re-selection. Double-notification risk exists because setSelection(Int) calls super which dispatches back to setSelection(Int, Boolean) via virtual dispatch, causing notifyReselected to fire twice; harmless in practice due to the dropdownOpened guard, but non-obvious.
app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt Adds VALUE_SELECT_PROFILE constant, reselectable layout, and inline spinner listener. The dropdownOpened flag correctly guards against spurious fires, and the selectionReady post-delay prevents the initial bind from triggering a change. Documented edge case: spurious onWindowFocusChanged can reset dropdownOpened before the user taps the item.
app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt Replaces magic "3" with VALUE_SELECT_PROFILE constant and passes the currently-selected profile as EXTRA_SELECTED when launching ProfileSelectActivity so the picker pre-selects the right row. Clean refactor with no functional regressions.
app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt Same constant-replacement and EXTRA_SELECTED wiring as GroupSettingsActivity, applied to the route outbound preference. No issues found.
app/src/main/res/layout/preference_dropdown_reselectable.xml Mirrors preference_dropdown_material.xml with the Spinner element replaced by ReselectableSpinner. Invisible spinner acts only as a popup anchor; visual row comes from the standard preference_material include.
app/src/main/res/xml/webdav_preferences.xml Cosmetic addition of app:icon attributes to five WebDAV preference rows. ic_baseline_folder_open_24 is the only new drawable; the other four already existed.
app/src/main/res/drawable/ic_baseline_folder_open_24.xml New 24dp vector drawable for the folder-open icon used in the WebDAV path preference row.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User taps Spinner row] --> B[OutboundPreference.onClick\ndropdownOpened = true]
    B --> C[Dropdown opens]
    C --> D{User selects item}
    D --> |different item| E[Spinner fires onItemSelected\nnewValue != value]
    D --> |same SELECT_PROFILE item| F[ReselectableSpinner.setSelection\nreselected = true]
    F --> G[notifyReselected fires\nonItemSelected callback]
    E --> H[OutboundPreference.onItemSelected\nselectionReady check]
    G --> H
    H --> I{reselectedProfile?\ndropdownOpened &&\nnewValue == value &&\nnewValue == VALUE_SELECT_PROFILE}
    I --> |yes - reselect| J[callChangeListener]
    I --> |no - new value| K{newValue != value?}
    K --> |yes| J
    K --> |no| L[no-op]
    J --> M[PreferenceChangeListener\nin Activity]
    M --> N{value == VALUE_SELECT_PROFILE?}
    N --> |yes| O[Launch ProfileSelectActivity\nwith EXTRA_SELECTED = current profile]
    N --> |no| P[Update preference value\nreturn true]
    O --> Q[dropdownOpened = false]
    P --> Q
    Q --> R[onWindowFocusChanged fires\nwhen popup closes\nonPopupClosed → dropdownOpened = false]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[User taps Spinner row] --> B[OutboundPreference.onClick\ndropdownOpened = true]
    B --> C[Dropdown opens]
    C --> D{User selects item}
    D --> |different item| E[Spinner fires onItemSelected\nnewValue != value]
    D --> |same SELECT_PROFILE item| F[ReselectableSpinner.setSelection\nreselected = true]
    F --> G[notifyReselected fires\nonItemSelected callback]
    E --> H[OutboundPreference.onItemSelected\nselectionReady check]
    G --> H
    H --> I{reselectedProfile?\ndropdownOpened &&\nnewValue == value &&\nnewValue == VALUE_SELECT_PROFILE}
    I --> |yes - reselect| J[callChangeListener]
    I --> |no - new value| K{newValue != value?}
    K --> |yes| J
    K --> |no| L[no-op]
    J --> M[PreferenceChangeListener\nin Activity]
    M --> N{value == VALUE_SELECT_PROFILE?}
    N --> |yes| O[Launch ProfileSelectActivity\nwith EXTRA_SELECTED = current profile]
    N --> |no| P[Update preference value\nreturn true]
    O --> Q[dropdownOpened = false]
    P --> Q
    Q --> R[onWindowFocusChanged fires\nwhen popup closes\nonPopupClosed → dropdownOpened = false]
Loading

Reviews (1): Last reviewed commit: "fix: reopen profile picker on reselect" | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 70b35cb6-abf9-4e77-a628-c3d50494b8dc

📥 Commits

Reviewing files that changed from the base of the PR and between 3028327 and 12a0b37.

📒 Files selected for processing (7)
  • app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt
  • app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt
  • app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt
  • app/src/main/java/io/nekohasekai/sagernet/widget/ReselectableSpinner.kt
  • app/src/main/res/drawable/ic_baseline_folder_open_24.xml
  • app/src/main/res/layout/preference_dropdown_reselectable.xml
  • app/src/main/res/xml/webdav_preferences.xml

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced profile selection in proxy group and route settings with improved dropdown interaction
    • Added support for reselecting profiles in front/landing proxy and route outbound configurations
  • Style

    • Added icons to WebDAV preference items for improved visual clarity

Walkthrough

Introduces ReselectableSpinner, a custom spinner that fires reselection events and exposes an onPopupClosed callback. OutboundPreference is expanded with a VALUE_SELECT_PROFILE constant (replacing the hardcoded "3"), dropdown-open state tracking, and spinner binding. GroupSettingsActivity and RouteSettingsActivity are updated to use the constant and pre-populate ProfileSelectActivity with the currently saved profile. WebDAV preference items receive icon attributes.

Changes

OutboundPreference reselection and VALUE_SELECT_PROFILE refactor

Layer / File(s) Summary
ReselectableSpinner widget and layout/drawable resources
app/src/main/java/io/nekohasekai/sagernet/widget/ReselectableSpinner.kt, app/src/main/res/layout/preference_dropdown_reselectable.xml, app/src/main/res/drawable/ic_baseline_folder_open_24.xml
New ReselectableSpinner extends AppCompatSpinner with onPopupClosed callback (via onWindowFocusChanged) and overridden setSelection overloads that re-dispatch selection events when the position is unchanged. A new layout wraps the hidden spinner as a popup anchor alongside preference_material, and a folder-open vector drawable is added.
OutboundPreference VALUE_SELECT_PROFILE constant and spinner binding
app/src/main/java/io/nekohasekai/sagernet/widget/OutboundPreference.kt
Defines VALUE_SELECT_PROFILE = "3", sets layoutResource to the new reselectable layout, overrides setValue to call notifyChanged on same-value sets, adds onClick/dropdownOpened tracking, and wires onBindViewHolder with a spinner listener that gates on selectionReady, handles the reselected-profile branch, and resets dropdown state. getSummary branches on VALUE_SELECT_PROFILE instead of the literal "3".
Settings activities VALUE_SELECT_PROFILE usage
app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt, app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt
Replaces all hardcoded "3"/3 comparisons in init, serialize, preference change listeners, and activity-result callbacks with OutboundPreference.VALUE_SELECT_PROFILE. Change listeners now optionally pass EXTRA_SELECTED to ProfileSelectActivity when a saved profile exists.

WebDAV preference icons

Layer / File(s) Summary
WebDAV preference icon additions
app/src/main/res/xml/webdav_preferences.xml
Adds app:icon attributes to the webdavServer, webdavUsername, webdavPassword, webdavPath, and webdavTest preference elements.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A spinner that knows when you pick it again,
No more lonely "3" strings buried in plain,
VALUE_SELECT_PROFILE hops into the light,
The folder icon opens, the dropdown feels right.
✨ Re-select and rejoice — the rabbit declares,
Named constants and callbacks: no magic, just care!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the two main changes: reopening profile picker on reselect and adding WebDAV icons.
Description check ✅ Passed The description comprehensively explains both commits, their purposes, implementation details, and testing status, directly relating to all changeset modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

@hawkff hawkff merged commit f7674c1 into main Jun 20, 2026
5 checks passed
@hawkff hawkff deleted the feature/picker-reselect-webdav-icons branch June 20, 2026 14:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants